﻿@using Blazor.Diagrams
@using Blazor.Diagrams.Components
@using Blazor.Diagrams.Components.Widgets
@using Blazor.Diagrams.Core.Anchors
@using Blazor.Diagrams.Core.Models
@using Blazor.Diagrams.Core.PathGenerators
@using Blazor.Diagrams.Core.Routers
@using Blazor.Diagrams.Options
@using FormBuilder.Components.Form
@using FormBuilder.Components.Shared
@using FormBuilder.Dialogs
@using FormBuilder.Factories
@using FormBuilder.Models
@using System.Diagnostics
@using FormBuilder.Models.Layout
@using FormBuilder.Services
@using PT = Blazor.Diagrams.Core.Geometry.Point;
@using SZ = Blazor.Diagrams.Core.Geometry.Size;
@using System.Text.Json
@inject IJSRuntime jsRuntime
@inject IFormBuilderJsService formBuilderJsService
@inject DialogService dialogService

<RadzenDialog />
<MonacoCompletion Context="@Context" />

<div class="header">
    <RadzenButton Icon="draft" ButtonStyle="ButtonStyle.Secondary" Text="Json" Click="@(() => ViewJson())" />
    @if (CanSave)
    {
        <RadzenButton Style="margin-left: 5px" Icon="save" Text="Save" Click="@(async() => await Save())" Disabled="@isSaveDisabled" />
    }
</div>

<RadzenSplitter>
    <!-- Workflow -->
    <RadzenSplitterPane Size="80%">
        <div id="workflowEditor" @ref="diagramContainer" class="diagram-container">
            <CascadingValue Value="Diagram" IsFixed="true">
                <DiagramCanvas>
                    <Widgets>
                        <GridWidget Size="30" Mode="GridMode.Line" BackgroundColor="white" />
                        <NavigatorWidget Width="200" Height="120" Class="border border-black bg-white absolute" Style="bottom: 15px; right: 15px;" />
                    </Widgets>
                </DiagramCanvas>
            </CascadingValue>
        </div>
    </RadzenSplitterPane>
    <!-- Properties -->
    <RadzenSplitterPane Size="20%">
        <!-- Property panel -->
        <div class="panel-header">
            <RadzenText TextStyle="TextStyle.H6" class="title">Properties</RadzenText>
        </div>
        <div class="panel-content">
            @if (panelType == FormPanelTypes.JSON)
            {
                <JsonPanelComponent @ref=jsonPanelComponent Json="@SerializedWorkflow" JsonChanged="HandleJsonChanged"></JsonPanelComponent>
            }
        </div>
    </RadzenSplitterPane>
</RadzenSplitter>

@code {
    [Parameter] public WorkflowRecord Workflow { get; set; }
    [Parameter] public List<FormRecord> Forms { get; set; }
    [Parameter] public List<WorkflowLayout> WorkflowLayouts { get; set; }
    [Parameter] public bool CanSave { get; set; } = true;
    [Parameter] public EventCallback<ActionState<WorkflowRecord, bool>> Saved { get; set; }

    [Inject] public NotificationService notificationService { get; set; }

    private WorkflowContext Context { get; set; }
    private ElementReference diagramContainer;
    private ElementReference svgRef;
    private (double x, double y) previousMousePosition;
    private bool isMoving = false;
    private bool isInitialized = false;
    private bool isSaveDisabled = false;
    private FormPanelTypes? panelType = null;
    private JsonPanelComponent jsonPanelComponent;
    private string SerializedWorkflow;

    private double spaceBetweenNodeXPx = 50;
    private double spaceBetweenNodeYPx = 150;
    private double paddingTopPx = 50;
    private SZ stepSize = new SZ(200, 70);
    private SZ endStepSize = new SZ(100, 100);
    private List<WorkflowLayout> availableLayouts { get; set; }

    private BlazorDiagram Diagram = new BlazorDiagram(new BlazorDiagramOptions
    {
        AllowMultiSelection = true,
        Zoom =
        {
            Enabled = true
        },
        Links =
        {
            DefaultRouter = new NormalRouter(),
            DefaultPathGenerator = new SmoothPathGenerator()
        }
    });

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);
        if (firstRender && Forms != null && Workflow != null && Context == null)
        {
            if (!Forms.Any(f => f.CorrelationId == Constants.EmptyStep.CorrelationId)) Forms.Add(Constants.EmptyStep);
            Context = WorkflowContext.CreateWorkflow(Workflow, Forms);
            var options = new BlazorDiagramOptions
            {
                AllowMultiSelection = true,
                Zoom =
                {
                    Enabled = false,
                },
                Links =
                {
                    DefaultRouter = new NormalRouter(),
                    DefaultPathGenerator = new SmoothPathGenerator()
                }
            };
            if (Diagram.Container == null) 
            {
                Diagram.ContainerChanged += OnContainerChanged;
            }
            else 
            {
                Init();
            }

            RefreshJson();
            Diagram.RegisterComponent<WorkflowStepNode, WorkflowStepNodeWidget>();
            Diagram.RegisterComponent<WorkflowStepChooserNode, WorkflowStepChooserNodeWidget>();
            Diagram.RegisterComponent<WorkflowConditionNode, WorkflowConditionNodeWidget>();
        }
    }

    private void HandleJsonChanged(string json)
    {
        var workflow = JsonSerializer.Deserialize<WorkflowRecord>(json);
        Refresh(workflow);
    }

    private void OnContainerChanged()
    {
        if (isInitialized) return;
        Init();
        isInitialized = true;
    }

    private void Init()
    {
        var firstStep = Context.GetFirstStep();
        RefreshAvailableLayouts();
        if(firstStep == null)
        {
            var record = new WorkflowStepChooserNode(AddNode, availableLayouts);
            AddNode((Diagram.Container.Width / 2) - (stepSize.Width / 2), paddingTopPx, record);
            return;
        }

        AddNode(firstStep);
    }

    private void AddNode(WorkflowStep newStep)
        => AddNode(Diagram.Container.Width / 2, paddingTopPx, newStep);

    private NodeModel AddNode(double startPosX, double startPosY, WorkflowStep newStep, NodeModel source = null, int index = 0, string description = null, bool isBlocked = false, WorkflowLink link = null)
    {
        var form = Context.Definition.Records.Single(r => r.CorrelationId == newStep.FormRecordCorrelationId);
        var node = new WorkflowStepNode(newStep, form, isBlocked, EditNode, link);
        if (source != null) 
        {
            AddLink(source, node, description);
        }

        node.AddPort(PortAlignment.Bottom);
        var pt = AddNode(startPosX, startPosY, node, source, index);
        startPosX = pt.X; 
        var stepLinks = Context.Definition.Workflow.Links.Where(l => l.SourceStepId == newStep.Id).ToList();
        var nbLinks = stepLinks.Count();
        startPosY = startPosY + spaceBetweenNodeYPx;
        startPosX = (stepLinks.Count() == 1) ? startPosX : (
            (stepLinks.Count() % 2 == 0 ? 
                (startPosX + (stepSize.Width / 2) - ((spaceBetweenNodeXPx + stepSize.Width) * (nbLinks / 2))) :
                (startPosX - (spaceBetweenNodeXPx + stepSize.Width) * (nbLinks / 2))
            )
        );
        int i = 0;
        var workflowLayout = WorkflowLayouts.SingleOrDefault(l => l.SourceFormCorrelationId == newStep.FormRecordCorrelationId);
        foreach(var stepLink in stepLinks)
        {
            var targetStepIds = stepLink.Targets.Select(t => t.TargetStepId);
            var nextSteps = Context.Definition.Workflow.Steps.Where(s => targetStepIds.Contains(s.Id));
            isBlocked = false;
            WorkflowLinkLayout lnk = null;
            if(workflowLayout != null)
            {
                var elt = form.GetChildByCorrelationId(stepLink.Source.EltId);
                lnk = workflowLayout.Links.SingleOrDefault(l => l.EltCorrelationId == elt.CorrelationId);
            }

            if(targetStepIds.Count() > 1)
            {
                var conditionNode = new WorkflowConditionNode();
                conditionNode.AddPort(PortAlignment.Bottom);
                AddNode(startPosX, startPosY, conditionNode, node, index);
                AddLink(node, conditionNode, "Condition");
                var newStartPosX = (stepLinks.Count() == 1) ? startPosX : (
                    (stepLinks.Count() % 2 == 0 ?
                        (startPosX + (stepSize.Width / 2) - ((spaceBetweenNodeXPx + stepSize.Width) * (nbLinks / 2))) :
                        (startPosX - (spaceBetweenNodeXPx + stepSize.Width) * (nbLinks / 2))
                    )
                );
                startPosY = startPosY + spaceBetweenNodeYPx;
                int si = 0;
                foreach (var targetStepId in targetStepIds)
                {
                    var nextStep = nextSteps.SingleOrDefault(s => s.Id == targetStepId);
                    var target = stepLink.Targets.Single(t => t.TargetStepId == targetStepId);
                    if (lnk != null && nextStep != null)
                    {
                        var selectedTarget = lnk.Targets.SingleOrDefault(t => t.TargetFormCorrelationId == nextStep.FormRecordCorrelationId);
                        isBlocked = selectedTarget != null;
                    }

                    if (nextStep != null)
                    {
                        AddNode(newStartPosX, startPosY, nextStep, conditionNode, si, target.Description, isBlocked, stepLink);
                    }
                    else
                    {
                        var record = new WorkflowStepChooserNode(AddNode, availableLayouts, stepLink);
                        AddNode(newStartPosX, startPosY, record, conditionNode, si);
                        AddLink(conditionNode, record, target.Description);
                    }

                    si++;
                }
            }
            else
            {
                var target = stepLink.Targets.First();
                var nextStep = nextSteps.FirstOrDefault();
                if (lnk != null && nextStep != null)
                {
                    var selectedTarget = lnk.Targets.SingleOrDefault(t => t.TargetFormCorrelationId == nextStep.FormRecordCorrelationId);
                    isBlocked = selectedTarget != null;
                }

                if (nextStep != null)
                {
                    AddNode(startPosX, startPosY, nextStep, node, i, target.Description, isBlocked, stepLink);
                }
                else
                {
                    var record = new WorkflowStepChooserNode(AddNode, availableLayouts, stepLink);
                    AddNode(startPosX, startPosY, record, node, i);
                    AddLink(node, record, target.Description);
                }
            }

            i++;
        }

        return node;
    }

    private void AddLink(NodeModel sourceNode, NodeModel targetNode, string description)
    {
        var sourceAnchor = new ShapeIntersectionAnchor(sourceNode);
        var topPort = targetNode.AddPort(PortAlignment.Top);
        var targetAnchor = new SinglePortAnchor(topPort);
        var model = new LinkModel(sourceAnchor, targetAnchor);
        model.Labels.Add(new LinkLabelModel(model, description));
        Diagram.Links.Add(model);
    }

    private PT AddNode<T>(double startPosX, double startPosY, T node, NodeModel source = null, int index = 0) where T : NodeModel
    {
        var posX = source == null ? startPosX : startPosX + (index * (stepSize.Width + spaceBetweenNodeXPx));
        var posY = startPosY;
        node.Position = new PT(posX, posY);
        Diagram.Nodes.Add(node);
        return node.Position;
    }

    private void AddNode(WorkflowStepChooserNode source, WorkflowStepChooserRecord record)
    {
        var sourceLink = Diagram.Links.FirstOrDefault(l =>
        {
            var targetModel = l.Target?.Model as PortModel;
            if (targetModel == null) return false;
            return targetModel.Parent is NodeModel targetNodeModel && targetNodeModel.Id == source.Id;
        });
        var sourceModel = sourceLink?.Source?.Model as NodeModel;
        WorkflowStep newStep = null;
        // 1. Generate and add a new step with its links.
        if(!record.IsEndStep) newStep = AddWorkflowStep(record.WorkflowCorrelationId, sourceModel, source.Link);
        else newStep = AddEndStep(sourceModel, source.Link); 
        // 2. Add node to diagram.
        AddNode(source.Position.X, source.Position.Y, newStep, sourceModel, 0, description: sourceLink?.Labels?.FirstOrDefault()?.Content, link: source.Link);
        Diagram.Nodes.Remove(source);
        RefreshJson();
    }

    private void EditNode(WorkflowStepNode node)
    {
        var sourceLink = Diagram.Links.FirstOrDefault(l =>
        {
            var targetModel = l.Target?.Model as PortModel;
            if (targetModel == null) return false;
            return targetModel.Parent is NodeModel targetNodeModel && targetNodeModel.Id == node.Id;
        });
        var sourceNode = sourceLink?.Source?.Model as NodeModel;
        // 1. Remove the current node and its links.
        Remove(node);
        // 2. Add the step chooser.
        var newStep = new WorkflowStepChooserNode(AddNode, availableLayouts, node.Link);
        // RefreshAvailableLayouts();
        AddNode(node.Position.X, node.Position.Y, newStep, sourceNode);
        if(sourceNode != null) AddLink(sourceNode, newStep, sourceLink.Labels.First().Content);
        RefreshJson();
    }

    private void Remove(NodeModel node)
    {
        if (node is WorkflowStepNode workflowStepNode) Remove(workflowStepNode.Step);
        var links = Diagram.Links.Where(l =>
        {
            var sourceModel = l.Source?.Model as NodeModel;
            return sourceModel != null && sourceModel.Id == node.Id;
        }).ToList();
        var targetSteps = links.Select(l => (l.Target.Model as PortModel).Parent as NodeModel);
        foreach (var targetStep in targetSteps)
            Remove(targetStep);

        foreach (var link in links)
            Diagram.Links.Remove(link);

        Diagram.Nodes.Remove(node);
    }

    private void Remove(WorkflowStep workflowStep)
    {
        var links = Context.Definition.Workflow.Links.Where(l => l.SourceStepId == workflowStep.Id).ToList();
        var targetedStepIds = links.SelectMany(l => l.Targets.Select(t => t.TargetStepId));
        var targetedSteps = Context.Definition.Workflow.Steps.Where(s => targetedStepIds.Contains(s.Id)).ToList();
        foreach (var targetedStep in targetedSteps)
            Remove(targetedStep);
        foreach(var link in links)
            Context.Definition.Workflow.Links.Remove(link);
        Context.Definition.Workflow.Steps.Remove(workflowStep);
    }

    private WorkflowStep AddEndStep(NodeModel sourceNodeModel = null, WorkflowLink link = null)
    {
        var formRecord = Forms.Single(r => r.CorrelationId == Constants.EmptyStep.CorrelationId);
        var newStep = new WorkflowStep
        {
            Id = Guid.NewGuid().ToString(),
            FormRecordCorrelationId = formRecord.CorrelationId
        };
        UpdateLink(newStep, sourceNodeModel, link);
        Context.Definition.Workflow.Steps.Add(newStep);
        return newStep;
    }

    private WorkflowStep AddWorkflowStep(string correlationId, NodeModel sourceNodeModel = null, WorkflowLink sourceLink = null)
    {
        var layout = WorkflowLayouts.Single(l => l.WorkflowCorrelationId == correlationId);
        var formRecord = Forms.Single(r => r.CorrelationId == layout.SourceFormCorrelationId);
        var newStep = new WorkflowStep
        {
            Id = Guid.NewGuid().ToString(),
            FormRecordCorrelationId = formRecord.CorrelationId
        };
        Context.Definition.Workflow.Steps.Add(newStep);
        AddWorkflowStepChildren(correlationId, newStep, sourceNodeModel, sourceLink);
        return newStep;
    }

    private void AddWorkflowStepChildren(string correlationId, WorkflowStep newStep, NodeModel sourceNodeModel = null, WorkflowLink sourceLink = null)
    {
        var layout = WorkflowLayouts.Single(l => l.WorkflowCorrelationId == correlationId);
        var formRecord = Forms.Single(r => r.CorrelationId == layout.SourceFormCorrelationId);
        var resolvedSteps = Forms.Where(f => layout.Links.Where(l => l.Targets != null).SelectMany(l => l.Targets.Select(t => t.TargetFormCorrelationId)).Where(t => !string.IsNullOrWhiteSpace(t)).Contains(f.CorrelationId))
            .Select(f =>
            {
                return new WorkflowStep
                {
                    Id = Guid.NewGuid().ToString(),
                    FormRecordCorrelationId = f.CorrelationId,
                };
            }).ToList();
        var links = layout.Links.Select(l =>
        {
            var eltId = formRecord.GetChildByCorrelationId(l.EltCorrelationId).CorrelationId;
            var source = new WorkflowLinkSource
            {
                EltId = eltId
            };
            var link = new WorkflowLink
            {
                Id = Guid.NewGuid().ToString(),
                SourceStepId = newStep.Id,
                Source = source,
                ActionType = l.ActionType,
                ActionParameter = l.ActionParameter,
                IsMainLink = l.IsMainLink,
                Targets = new List<WorkflowLinkTarget>()
            };
            foreach (var target in l.Targets)
            {
                var form = Forms.SingleOrDefault(r => r.CorrelationId == target.TargetFormCorrelationId);
                var targetStep = resolvedSteps.SingleOrDefault(s => s.FormRecordCorrelationId == form?.CorrelationId);
                var targetStepId = targetStep?.Id;
                link.Targets.Add(new WorkflowLinkTarget
                {
                    TargetStepId = targetStepId,
                    Condition = target.Condition,
                    Description = target.Description
                });
            }

            return link;
        });
        Context.Definition.Workflow.Steps.AddRange(resolvedSteps);
        Context.Definition.Workflow.Links.AddRange(links);
        UpdateLink(newStep, sourceNodeModel, sourceLink);
        var resolvedLayouts = WorkflowLayouts.Where(l => l.SourceFormCorrelationId != FormBuilder.Constants.EmptyStep.CorrelationId && resolvedSteps.Any(s => s.FormRecordCorrelationId == l.SourceFormCorrelationId));
        foreach(var resolvedStep in resolvedSteps.Where(s => s.FormRecordCorrelationId != FormBuilder.Constants.EmptyStep.CorrelationId))
        {
            var resolvedLayout = WorkflowLayouts.Single(l => l.SourceFormCorrelationId == resolvedStep.FormRecordCorrelationId);
            AddWorkflowStepChildren(resolvedLayout.WorkflowCorrelationId, resolvedStep, sourceNodeModel);   
        }
    }

    private void UpdateLink(WorkflowStep newStep, NodeModel sourceNodeModel, WorkflowLink sourceLink = null)
    {
        var sourceWorkflowStepNode = sourceNodeModel as WorkflowStepNode;
        var conditionNode = sourceNodeModel as WorkflowConditionNode;
        if (sourceLink != null)
        {
            var link = Context.Definition.Workflow.Links.SingleOrDefault(l => l.Id == sourceLink.Id);
            if(link != null) 
            {
                if(sourceWorkflowStepNode != null)
                {
                    link.Targets.First().TargetStepId = newStep.Id;
                }
                else if(conditionNode != null)
                {
                    link.Targets.Last().TargetStepId = newStep.Id;
                }
            }
        }

    }

    private void RefreshAvailableLayouts()
    {
        availableLayouts = WorkflowLayouts.Where(l =>
        {
            var form = Forms.Single(r => r.CorrelationId == l.SourceFormCorrelationId);
            return form.ActAsStep;
        }).ToList();
    }

    private List<WorkflowLink> GetEmptyLinks()
        => Context.Definition.Workflow.Links.Where(l => l.Targets == null || !l.Targets.Any()).ToList();

    private async Task Save()
    {
        isSaveDisabled = true;
        await Saved.InvokeAsync(new ActionState<WorkflowRecord, bool>((b) =>
        {
            notificationService.Notify(new NotificationMessage { Severity = NotificationSeverity.Success, Summary = "The workflow is saved" });
            isSaveDisabled = false;
        }, Workflow));
    }

    private void ViewJson()
    {
        Serialize();
        panelType = FormPanelTypes.JSON;
    }

    private void RefreshJson()
    {
        Serialize();
        if (jsonPanelComponent != null) jsonPanelComponent.Refresh(SerializedWorkflow);
    }

    private void Serialize()
        => SerializedWorkflow = JsonSerializer.Serialize(Context.Definition.Workflow, new JsonSerializerOptions { WriteIndented = true });

    private void Refresh(WorkflowRecord workflow)
    {
        CleanDiagram();
        Context = WorkflowContext.CreateWorkflow(workflow, Forms);
        Workflow = workflow;
        Init();
    }

    private void CleanDiagram()
    {
        foreach (var record in Diagram.Nodes.ToList())
            Diagram.Nodes.Remove(record);
        foreach (var link in Diagram.Links.ToList())
            Diagram.Links.Remove(link);
    }

    private FormRecord GetFormRecord(string name)
        => Context.Definition.Records.Single(t => t.Name == name);
}